package http

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"github.com/google/uuid"
	"github.com/pkg/errors"
	"go.uber.org/zap"
	"gobase/logger"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

type Client struct {
	ctx          context.Context
	cansel       context.CancelFunc
	clt          *http.Client
	tr           *http.Transport
	debug        bool
	timeout      time.Duration
	header       http.Header
	RequestHook  func(traceID string, r *Request) error
	ResponseHook func(traceID string, r *Response) error
}

type Request struct {
	ctx         context.Context
	cansel      context.CancelFunc
	clt         *Client
	traceID     string
	url         string
	method      string
	param       url.Values
	header      http.Header
	data        interface{}
	bData       []byte
	timeout     time.Duration
	noKeepalive bool
	raw         *http.Request
}

type Response struct {
	ctx            context.Context
	cansel         context.CancelFunc
	traceID        string
	raw            *http.Response
	Status         int
	Msg            string
	responseHeader http.Header
	body           []byte
	err            error
}

func NewClient() *Client {
	ctx, cancel := context.WithCancel(context.Background())
	return &Client{
		ctx:    ctx,
		cansel: cancel,
		clt:    &http.Client{},
		tr:     &http.Transport{},
		header: http.Header{},
	}
}

func DefaultClient() *Client {
	clt := NewClient()
	clt.SetTimeout(60 * time.Second)
	clt.SetHeader("Content-Type", "application/json")
	return clt
}

func (c *Client) SetDebug(b bool) {
	c.debug = b
	if c.debug {
		logger.Warn("!!! NOW HTTP CLIENT IS DEBUG MODEL, PLEASE DO NOT USE IN PRODUCTION ENVIRONMENT !!!")
	}
}

func (c *Client) IsDebug() bool {
	return c.debug
}

func (c *Client) SetHttpClient(hc *http.Client) {
	c.clt = hc
}

func (c *Client) GetHttpClient() *http.Client {
	return c.clt
}

func (c *Client) SetHeader(key string, val string) {
	c.header.Set(key, val)
	if c.debug {
		logger.Debug("Client header", zap.Any("header", c.header))
	}
}

func (c *Client) GetHeader() http.Header {
	return c.header
}

func (c *Client) GetHeaderWith(k string) string {
	return c.header.Get(k)
}

func (c *Client) SetTimeout(t time.Duration) {
	c.timeout = t
	c.clt.Timeout = t
	if c.debug {
		logger.Debug("Client timeout", zap.Duration("timeout", c.timeout), zap.Duration("clt.Timeout", c.clt.Timeout))
	}
}

func (c *Client) GetTimeout() time.Duration {
	return c.timeout
}

func (c *Client) SetCA(certPath string) error {
	if c.debug {
		logger.Debug("CA file", zap.String("path", certPath))
	}
	pool := x509.NewCertPool()
	caCrt, err := os.ReadFile(certPath)
	if err != nil {
		logger.Error("open cert file failed", zap.Error(err), zap.String("file path", certPath))
		return err
	}
	pool.AppendCertsFromPEM(caCrt)

	if c.tr.TLSClientConfig != nil {
		c.tr.TLSClientConfig.RootCAs = pool
	} else {
		c.tr.TLSClientConfig = &tls.Config{RootCAs: pool}
	}
	c.tr.DisableCompression = true
	c.clt.Transport = c.tr
	return nil
}

func (c Client) SetCert(certFile string, keyFile string) error {
	if c.debug {
		logger.Debug("cert files", zap.String("certFile", certFile), zap.String("keyFile", keyFile))
	}
	cliCrt, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		logger.Error("load cert failed", zap.Error(err), zap.String("certFile", certFile), zap.String("certFile", keyFile))
		return err
	}
	if c.tr.TLSClientConfig != nil {
		c.tr.TLSClientConfig.Certificates = []tls.Certificate{cliCrt}
	} else {
		c.tr.TLSClientConfig = &tls.Config{Certificates: []tls.Certificate{cliCrt}}
	}
	c.clt.Transport = c.tr
	return nil
}

func (c Client) execute(r *Request) *Response {
	tStart := time.Now()
	var resp = &Response{
		ctx:     r.ctx,
		cansel:  r.cansel,
		traceID: r.traceID,
	}
	response, err := c.clt.Do(r.raw)
	defer func() {
		//if c.debug {
		//	logger.Debug("Response Request context", zap.Any("ctx", response.Request.Context()))
		//}
		if response != nil {
			_ = response.Body.Close()
		}
	}()
	if err != nil {
		logger.Error("Client do request failed", zap.Error(err), zap.Any("httpRequest", r.raw))
		return &Response{err: err, Status: 500}
	}
	body := response.Body
	if resp.body, err = io.ReadAll(body); err != nil {
		logger.Error("Response read body failed", zap.Error(err))
		return &Response{err: err, Status: 500}
	}
	resp.raw = response
	resp.Status = response.StatusCode
	resp.Msg = response.Status
	resp.responseHeader = response.Header
	if c.ResponseHook != nil {
		if err := c.ResponseHook(r.traceID, resp); err != nil {
			logger.Error("Response hook failed", zap.Error(err))
			return &Response{err: err, Status: 500}
		}
	}
	tEnd := time.Since(tStart)
	if c.debug {
		logger.Debug("Response result", zap.Any("responseHeader", resp.responseHeader), zap.Binary("body", resp.body))
	}
	logger.Info("Request successful", zap.Int("StatusCode", resp.Status), zap.Duration("costTime", tEnd), zap.Any("context", resp.ctx))
	return resp
}

func (c *Client) NewRequest() *Request {
	ctx, cancel := context.WithCancel(context.Background())
	return &Request{
		ctx:     ctx,
		cansel:  cancel,
		clt:     c,
		traceID: uuid.New().String(),
		header:  c.header.Clone(),
		param:   url.Values{},
		timeout: c.timeout,
	}
}

func (r *Request) SetMethod(m string) *Request {
	switch m {
	case "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH":
		r.method = m
	default:
		r.method = "GET"
	}
	r.method = m
	return r
}

func (r *Request) GetMethod() string {
	return r.method
}

func (r *Request) SetUrl(url string) *Request {
	r.url = url
	return r
}

func (r *Request) GetUrl() string {
	return r.url
}

func (r *Request) SetTimeout(t time.Duration) *Request {
	r.timeout = t
	if r.clt.debug {
		logger.Debug("Request timeout", zap.String("traceID", r.traceID), zap.Duration("time", r.timeout))
	}
	return r
}

func (r *Request) GetTimeout() time.Duration {
	return r.timeout
}

func (r *Request) SetWithoutKeepalive(b bool) *Request {
	r.noKeepalive = b
	if r.clt.debug {
		logger.Debug("Request not keepalive", zap.String("traceID", r.traceID), zap.Bool("is", r.noKeepalive))
	}
	return r
}

func (r *Request) IsWithoutKeepalive() bool {
	return r.noKeepalive
}

func (r *Request) SetHeader(key string, val string) *Request {
	r.header.Set(key, val)
	if r.clt.debug {
		logger.Debug("Request Header", zap.String("traceID", r.traceID), zap.Any("header", r.header))
	}
	return r
}

func (r *Request) GetHeader() http.Header {
	return r.header
}

func (r *Request) GetHeaderWith(k string) string {
	return r.header.Get(k)
}

func (r *Request) SetParam(key string, val string) *Request {
	r.param.Set(key, val)
	if r.clt.debug {
		logger.Debug("Request param", zap.String("traceID", r.traceID), zap.Any("param", r.param))
	}
	return r
}

func (r *Request) SetParams(param map[string]string) *Request {
	for k, v := range param {
		r.param.Set(k, v)
	}
	if r.clt.debug {
		logger.Debug("Request param", zap.String("traceID", r.traceID), zap.Any("param", r.param))
	}
	return r
}

func (r *Request) GetParam() url.Values {
	return r.param
}

func (r *Request) GetParamWith(k string) string {
	return r.param.Get(k)
}

func (r *Request) SetData(data interface{}) *Request {
	r.data = data
	if r.clt.debug {
		logger.Debug("Request data", zap.String("traceID", r.traceID), zap.Any("data", r.data))
	}
	return r
}

func (r *Request) GetData() interface{} {
	return r.data
}

func (r *Request) GetRaw() *http.Request {
	return r.raw
}

func (r *Request) createReq() error {
	tStart := time.Now()
	reqURL, err := url.Parse(r.url)
	if err != nil {
		logger.Error("Request parse url error", zap.Error(err), zap.String("traceID", r.traceID), zap.String("url", r.url))
		return err
	}
	if reqURL.Scheme != "https" && reqURL.Scheme != "http" {
		if reqURL.Opaque != "" {
			reqURL, err = url.Parse("http://" + r.url)
			if err != nil {
				logger.Error("Request parse url error", zap.Error(err), zap.String("traceID", r.traceID), zap.String("url", r.url))
				return err
			}
		} else {
			reqURL.Scheme = "http"
		}
	}
	query := make(url.Values)
	for k, v := range r.param {
		for _, iv := range v {
			query.Add(k, iv)
		}
	}
	if len(strings.TrimSpace(reqURL.RawQuery)) == 0 {
		reqURL.RawQuery = query.Encode()
	} else {
		reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode()
	}
	r.url = reqURL.String()
	if r.clt.debug {
		logger.Debug("Request parse url", zap.String("traceID", r.traceID), zap.Any("Scheme", reqURL.Scheme), zap.Any("Host", reqURL.Host), zap.Any("Port", reqURL.Path), zap.Any("Query", reqURL.Query()))
	}
	logger.Info("Request url", zap.String("traceID", r.traceID), zap.String("url", r.url))
	if r.data != nil {
		r.bData, err = json.Marshal(r.data)
		if err != nil {
			logger.Error("Request data json Marshal failed", zap.Error(err), zap.String("traceID", r.traceID), zap.Any("data", r.data))
			return err
		}
		body := bytes.NewReader(r.bData)
		r.raw, err = http.NewRequest(r.method, r.url, body)
		if err != nil {
			logger.Error("Request init failed", zap.Error(err), zap.String("traceID", r.traceID), zap.String("method", r.method), zap.String("url", r.url), zap.Any("data", r.data), zap.Binary("byteData", r.bData))
			return err
		}
	} else {
		r.raw, err = http.NewRequest(r.method, r.url, nil)
		if err != nil {
			logger.Error("Request init failed", zap.Error(err), zap.String("traceID", r.traceID), zap.String("method", r.method), zap.String("url", r.url), zap.Any("data", r.data), zap.Binary("byteData", r.bData))
			return err
		}
	}
	r.raw.Header = r.header.Clone()
	if r.noKeepalive {
		r.raw.Close = true
	}
	r.raw = r.raw.WithContext(r.ctx)
	if r.clt.RequestHook != nil {
		if err := r.clt.RequestHook(r.traceID, r); err != nil {
			logger.Error("Request hook callback failed", zap.Error(err), zap.String("traceID", r.traceID))
			return err
		}
	}
	tEnd := time.Since(tStart)
	if r.clt.debug {
		logger.Debug("Request created", zap.String("traceID", r.traceID), zap.String("method", r.method), zap.String("url", r.url), zap.Any("header", r.header), zap.Binary("data", r.bData), zap.Duration("costTime", tEnd))
	}
	return nil
}

func (r *Request) Get(url string) *Response {
	r.url = url
	r.method = "GET"
	if r.timeout > 0 {
		r.ctx, r.cansel = context.WithTimeout(r.ctx, r.timeout)
	}
	defer r.cansel()
	if err := r.createReq(); err != nil {
		logger.Error("Request create failed", zap.Error(err))
		return &Response{err: err, Status: 500}
	}
	return r.clt.execute(r)
}

func (r *Request) Post(url string) *Response {
	r.url = url
	r.method = "POST"
	if r.timeout > 0 {
		r.ctx, r.cansel = context.WithTimeout(r.ctx, r.timeout)
	}
	defer r.cansel()
	if err := r.createReq(); err != nil {
		logger.Error("Request create failed", zap.Error(err))
		return &Response{err: err, Status: 500}
	}
	return r.clt.execute(r)
}

func (r *Request) Put(url string) *Response {
	r.url = url
	r.method = "PUT"
	if r.timeout > 0 {
		r.ctx, r.cansel = context.WithTimeout(r.ctx, r.timeout)
	}
	defer r.cansel()
	if err := r.createReq(); err != nil {
		logger.Error("Request create failed", zap.Error(err))
		return &Response{err: err, Status: 500}
	}
	return r.clt.execute(r)
}

func (r *Request) Delete(url string) *Response {
	r.url = url
	r.method = "DELETE"
	if r.timeout > 0 {
		r.ctx, r.cansel = context.WithTimeout(r.ctx, r.timeout)
	}
	defer r.cansel()
	if err := r.createReq(); err != nil {
		logger.Error("Request create failed", zap.Error(err))
		return &Response{err: err, Status: 500}
	}
	return r.clt.execute(r)
}

func (r *Response) Err() error {
	return r.err
}

func (r *Response) GetBody() []byte {
	return r.body
}

func (r *Response) GetString() string {
	return string(r.body)
}

func (r *Response) GetJson(v interface{}) error {
	return json.Unmarshal(r.body, v)
}

func (r *Response) GetHeader() http.Header {
	return r.responseHeader
}

func (r *Response) GetHeaderWith(k string) string {
	return r.responseHeader.Get(k)
}

func (r *Response) GetRaw() *http.Response {
	return r.raw
}

func loggerErrorWarp(msg string, err error, fields ...zap.Field) error {
	args := append([]zap.Field{zap.Error(err)}, fields...)
	logger.Error(msg, args...)
	return errors.Wrap(err, msg)
}
